﻿Imports System
Imports System.Collections.Generic 
Imports System.Text 
Imports System.Windows.Forms 
Imports System.Windows.Forms.VisualStyles 
Imports System.Drawing 
Imports System.ComponentModel 

''' <summary>
''' Extended Toolstrip control; adds click-through functionality
''' </summary>
''' <remarks>See http://blogs.msdn.com/rickbrew/archive/2006/01/09/511003.aspx</remarks>
Public Class ToolStripEx
    Inherits ToolStrip

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal items() As ToolStripItem)
        MyBase.New(items)
    End Sub

    Private _ClickThrough As Boolean = True
    Public Property ClickThrough() As Boolean
        Get
            Return (_ClickThrough)
        End Get
        Set(ByVal value As Boolean)
            _ClickThrough = value
        End Set
    End Property

    Private Const WM_MOUSEACTIVATE = &H21
    Private Const MA_ACTIVATE = 1
    Private Const MA_ACTIVATEANDEAT = 2
    Private Const MA_NOACTIVATE = 3
    Private Const MA_NOACTIVATEANDEAT = 4

    ''' <summary>
    ''' Implement click-through functionality so don't have to click inactive form to activate it, then click again to perform action
    ''' </summary>
    ''' <param name="m"></param>
    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If _ClickThrough And m.Msg = WM_MOUSEACTIVATE And m.Result = MA_ACTIVATEANDEAT Then m.Result = MA_ACTIVATE
    End Sub

End Class

''' <summary>
''' Extended Menustrip control; adds click-through functionality
''' </summary>
''' <remarks>See http://blogs.msdn.com/rickbrew/archive/2006/01/09/511003.aspx</remarks>
Public Class MenuStripEx
    Inherits MenuStrip

    Private _ClickThrough As Boolean = True
    Public Property ClickThrough() As Boolean
        Get
            Return (_ClickThrough)
        End Get
        Set(ByVal value As Boolean)
            _ClickThrough = value
        End Set
    End Property

    Private Const WM_MOUSEACTIVATE = &H21
    Private Const MA_ACTIVATE = 1
    Private Const MA_ACTIVATEANDEAT = 2
    Private Const MA_NOACTIVATE = 3
    Private Const MA_NOACTIVATEANDEAT = 4

    ''' <summary>
    ''' Implement click-through functionality so don't have to click inactive form to activate it, then click again to perform action
    ''' </summary>
    ''' <param name="m"></param>
    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If _ClickThrough And m.Msg = WM_MOUSEACTIVATE And m.Result = MA_ACTIVATEANDEAT Then m.Result = MA_ACTIVATE
    End Sub

End Class

'Original: http://blogs.msdn.com/jfoscoding/articles/491523.aspx 
'Wyatt's fixes: http://wyday.com/blog/csharp-splitbutton 
'http://wyday.com/blog/2007/splitbutton-in-c-passing-the-aic-test/
Public Class SplitButton
    Inherits Button
    Private _state As PushButtonState


    Private Const SplitSectionWidth As Integer = 14

    Private Shared BorderSize As Integer = SystemInformation.Border3DSize.Width * 2
    Private dropDownRectangle As New Rectangle()
    Private m_showSplit As Boolean = False

    Private isSplitMenuVisible As Boolean = False
    Private isMouseOutside As Boolean = False

    Private m_SplitMenuStrip As ContextMenuStrip = Nothing
    Private m_SplitMenu As ContextMenu = Nothing

    Private textFormatFlags As TextFormatFlags = textFormatFlags.[Default]

    Public Sub New()
        If Not IsDesignMode(Me) Then Me.AutoSize = True
    End Sub

#Region "Properties"

    <Browsable(False)> _
    Public Overloads Overrides Property ContextMenuStrip() As ContextMenuStrip
        Get
            Return m_SplitMenuStrip
        End Get
        Set(ByVal value As ContextMenuStrip)
            m_SplitMenuStrip = value
        End Set
    End Property

    '<DefaultValue(Nothing)> _
    Public Property SplitMenu() As ContextMenu
        Get
            Return m_SplitMenu
        End Get
        Set(ByVal value As ContextMenu)
            'remove the event handlers for the old SplitMenu 
            If m_SplitMenu IsNot Nothing Then
                RemoveHandler m_SplitMenu.Popup, AddressOf SplitMenu_Popup
            End If

            'add the event handlers for the new SplitMenu 
            If value IsNot Nothing Then
                ShowSplit = True
                AddHandler value.Popup, AddressOf SplitMenu_Popup
            Else
                ShowSplit = False
            End If

            m_SplitMenu = value
        End Set
    End Property

    '<DefaultValue(Nothing)> _
    Public Property SplitMenuStrip() As ContextMenuStrip
        Get
            Return m_SplitMenuStrip
        End Get
        Set(ByVal value As ContextMenuStrip)
            'remove the event handlers for the old SplitMenuStrip 
            If m_SplitMenuStrip IsNot Nothing Then
                RemoveHandler m_SplitMenuStrip.Closing, AddressOf SplitMenuStrip_Closing
                RemoveHandler m_SplitMenuStrip.Opening, AddressOf SplitMenuStrip_Opening
            End If

            'add the event handlers for the new SplitMenuStrip 
            If value IsNot Nothing Then
                ShowSplit = True
                AddHandler value.Closing, AddressOf SplitMenuStrip_Closing
                AddHandler value.Opening, AddressOf SplitMenuStrip_Opening
            Else
                ShowSplit = False
            End If


            m_SplitMenuStrip = value
        End Set
    End Property

    <DefaultValue(False)> _
    Public WriteOnly Property ShowSplit() As Boolean
        Set(ByVal value As Boolean)
            If value <> m_showSplit Then
                m_showSplit = value
                Invalidate()
                If Me.Parent IsNot Nothing Then
                    Me.Parent.PerformLayout()
                End If
            End If
        End Set
    End Property

    Private Property State() As PushButtonState
        Get
            Return _state
        End Get
        Set(ByVal value As PushButtonState)
            If Not _state.Equals(value) Then
                _state = value
                Invalidate()
            End If
        End Set
    End Property

#End Region

    Protected Overloads Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean
        If keyData.Equals(Keys.Down) AndAlso m_showSplit Then
            Return True
        Else

            Return MyBase.IsInputKey(keyData)
        End If
    End Function

    Protected Overloads Overrides Sub OnGotFocus(ByVal e As System.EventArgs)
        If Not m_showSplit Then
            MyBase.OnGotFocus(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            State = PushButtonState.[Default]
        End If
    End Sub

    Protected Overloads Overrides Sub OnKeyDown(ByVal kevent As KeyEventArgs)
        If m_showSplit Then
            If kevent.KeyCode.Equals(Keys.Down) AndAlso Not isSplitMenuVisible Then
                ShowContextMenuStrip()
            ElseIf kevent.KeyCode.Equals(Keys.Space) AndAlso kevent.Modifiers = Keys.None Then

                State = PushButtonState.Pressed
            End If
        End If

        MyBase.OnKeyDown(kevent)
    End Sub

    Protected Overloads Overrides Sub OnKeyUp(ByVal kevent As KeyEventArgs)
        If kevent.KeyCode.Equals(Keys.Space) Then
            If Control.MouseButtons = MouseButtons.None Then
                State = PushButtonState.Normal
            End If
        ElseIf kevent.KeyCode.Equals(Keys.Apps) Then
            If Control.MouseButtons = MouseButtons.None AndAlso Not isSplitMenuVisible Then
                ShowContextMenuStrip()
            End If
        End If

        MyBase.OnKeyUp(kevent)
    End Sub

    Protected Overloads Overrides Sub OnEnabledChanged(ByVal e As EventArgs)
        If Enabled Then
            State = PushButtonState.Normal
        Else
            State = PushButtonState.Disabled
        End If

        MyBase.OnEnabledChanged(e)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnLostFocus(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            State = PushButtonState.Normal
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseEnter(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseEnter(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            isMouseOutside = False
            State = PushButtonState.Hot
        End If

    End Sub

    Protected Overloads Overrides Sub OnMouseLeave(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseLeave(e)
            Return
        End If

        '!(State.Equals(PushButtonState.Pressed) && isSplitMenuVisible && ClientRectangle.Contains(PointToClient(Cursor.Position)) ) 

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            If Focused Then
                State = PushButtonState.[Default]
            Else

                State = PushButtonState.Normal
            End If
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseMove(ByVal mevent As MouseEventArgs)
        If Not ClientRectangle.Contains(mevent.Location) Then
            If State.Equals(PushButtonState.Pressed) AndAlso Not isSplitMenuVisible Then
                isMouseOutside = True
                State = PushButtonState.Hot
            End If
        ElseIf isMouseOutside Then
            'mouse is now inside the button, but was previously outside 
            isMouseOutside = False
            State = PushButtonState.Pressed
        End If

        MyBase.OnMouseMove(mevent)
    End Sub

    Protected Overloads Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseDown(e)
            Return
        End If

        If dropDownRectangle.Contains(e.Location) AndAlso Not isSplitMenuVisible AndAlso e.Button = MouseButtons.Left Then
            If State <> PushButtonState.Pressed Then
                ShowContextMenuStrip()
            Else
                State = PushButtonState.Hot
            End If
        Else
            State = PushButtonState.Pressed
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseUp(mevent)
            Return
        End If

        ' if the right button was released inside the button 
        If mevent.Button = MouseButtons.Right AndAlso ClientRectangle.Contains(mevent.Location) AndAlso Not isSplitMenuVisible Then
            ShowContextMenuStrip()
        ElseIf m_SplitMenuStrip Is Nothing AndAlso m_SplitMenu Is Nothing OrElse Not isSplitMenuVisible Then
            SetButtonDrawState()

            If ClientRectangle.Contains(mevent.Location) AndAlso Not dropDownRectangle.Contains(mevent.Location) Then
                OnClick(New EventArgs)
            End If
        End If
    End Sub

    Protected Overloads Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
        MyBase.OnPaint(pevent)

        If Not m_showSplit Then
            Return
        End If

        Dim g As Graphics = pevent.Graphics
        Dim bounds As Rectangle = Me.ClientRectangle

        ' draw the button background as according to the current state. 
        If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
            Dim backgroundBounds As Rectangle = bounds
            backgroundBounds.Inflate(-1, -1)
            ButtonRenderer.DrawButton(g, backgroundBounds, State)

            ' button renderer doesnt draw the black frame when themes are off =( 
            g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
        Else
            ButtonRenderer.DrawButton(g, bounds, State)
        End If

        ' calculate the current dropdown rectangle. 
        dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

        Dim internalBorder As Integer = BorderSize
        Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

        Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)


        If RightToLeft = RightToLeft.Yes Then
            dropDownRectangle.X = bounds.Left + 1
            focusRect.X = dropDownRectangle.Right

            If drawSplitLine Then
                ' draw two lines at the edge of the dropdown button 
                g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize)
                g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize)
            End If
        Else
            If drawSplitLine Then
                ' draw two lines at the edge of the dropdown button 
                g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize)
                g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize)
            End If
        End If

        ' Draw an arrow in the correct location 
        PaintArrow(g, dropDownRectangle)

        'paint the image and text in the "button" part of the splitButton 
        PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

        ' draw the focus rectangle. 
        If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
            ControlPaint.DrawFocusRectangle(g, focusRect)
        End If
    End Sub

    Private Sub PaintTextandImage(ByVal g As Graphics, ByVal bounds As Rectangle)
        ' Figure out where our text and image should go 
        Dim text_rectangle As Rectangle
        Dim image_rectangle As Rectangle

        CalculateButtonTextAndImageLayout(bounds, text_rectangle, image_rectangle)

        'draw the image 
        If Image IsNot Nothing Then
            If Enabled Then
                g.DrawImage(Image, image_rectangle.X, image_rectangle.Y, Image.Width, Image.Height)
            Else
                ControlPaint.DrawImageDisabled(g, Image, image_rectangle.X, image_rectangle.Y, BackColor)
            End If
        End If

        ' If we dont' use mnemonic, set formatFlag to NoPrefix as this will show ampersand. 
        If Not UseMnemonic Then
            textFormatFlags = textFormatFlags Or textFormatFlags.NoPrefix
        ElseIf Not ShowKeyboardCues Then
            textFormatFlags = textFormatFlags Or textFormatFlags.HidePrefix
        End If

        'draw the text 
        If Not String.IsNullOrEmpty(Me.Text) Then
            If Enabled Then
                TextRenderer.DrawText(g, Text, Font, text_rectangle, SystemColors.ControlText, textFormatFlags)
            Else
                ControlPaint.DrawStringDisabled(g, Text, Font, BackColor, text_rectangle, textFormatFlags)
            End If
        End If
    End Sub

    Private Sub PaintArrow(ByVal g As Graphics, ByVal dropDownRect As Rectangle)
        Dim middle As New Point(Convert.ToInt32(dropDownRect.Left + dropDownRect.Width / 2), Convert.ToInt32(dropDownRect.Top + dropDownRect.Height / 2))

        'if the width is odd - favor pushing it over one pixel right. 
        middle.X += (dropDownRect.Width Mod 2)

        Dim arrow As Point() = New Point() {New Point(middle.X - 2, middle.Y - 1), New Point(middle.X + 3, middle.Y - 1), New Point(middle.X, middle.Y + 2)}

        If Enabled Then
            g.FillPolygon(SystemBrushes.ControlText, arrow)
        Else
            g.FillPolygon(SystemBrushes.ButtonShadow, arrow)
        End If
    End Sub

    Public Overloads Overrides Function GetPreferredSize(ByVal proposedSize As Size) As Size
        Dim preferredSize As Size = MyBase.GetPreferredSize(proposedSize)

        'autosize correctly for splitbuttons 
        If m_showSplit Then
            If AutoSize Then
                Return CalculateButtonAutoSize()
            ElseIf Not String.IsNullOrEmpty(Text) AndAlso TextRenderer.MeasureText(Text, Font).Width + SplitSectionWidth > preferredSize.Width Then
                Return preferredSize + New Size(SplitSectionWidth + BorderSize * 2, 0)
            End If
        End If

        Return preferredSize
    End Function

    Private Function CalculateButtonAutoSize() As Size
        Dim ret_size As Size = Size.Empty
        Dim text_size As Size = TextRenderer.MeasureText(Text, Font)
        Dim image_size As Size = Size.Empty
        If Image IsNot Nothing Then image_size = Image.Size

        ' Pad the text size 
        If Text.Length <> 0 Then
            text_size.Height += 4
            text_size.Width += 4
        End If

        Select Case TextImageRelation
            Case TextImageRelation.Overlay
                ret_size.Height = Math.Max(IIf(Text.Length = 0, 0, text_size.Height), image_size.Height)
                ret_size.Width = Math.Max(text_size.Width, image_size.Width)
                Exit Select
            Case TextImageRelation.ImageAboveText, TextImageRelation.TextAboveImage
                ret_size.Height = text_size.Height + image_size.Height
                ret_size.Width = Math.Max(text_size.Width, image_size.Width)
                Exit Select
            Case TextImageRelation.ImageBeforeText, TextImageRelation.TextBeforeImage
                ret_size.Height = Math.Max(text_size.Height, image_size.Height)
                ret_size.Width = text_size.Width + image_size.Width
                Exit Select
        End Select

        ' Pad the result 
        ret_size.Height += (Padding.Vertical + 6)
        ret_size.Width += (Padding.Horizontal + 6)

        'pad the splitButton arrow region 
        If m_showSplit Then
            ret_size.Width += SplitSectionWidth
        End If

        Return ret_size
    End Function

#Region "Button Layout Calculations"

    'The following layout functions were taken from Mono's Windows.Forms 
    'implementation, specifically "ThemeWin32Classic.cs", 
    'then modified to fit the context of this splitButton 

    Private Sub CalculateButtonTextAndImageLayout(ByRef content_rect As Rectangle, ByRef textRectangle As Rectangle, ByRef imageRectangle As Rectangle)
        Dim text_size As Size = TextRenderer.MeasureText(Text, Font, content_rect.Size, textFormatFlags)
        Dim image_size As Size = Size.Empty
        If Image IsNot Nothing Then image_size = Image.Size
        'Dim image_size As Size = IIf(Image Is Nothing, Size.Empty, Image.Size)

        textRectangle = Rectangle.Empty
        imageRectangle = Rectangle.Empty

        Select Case TextImageRelation
            Case TextImageRelation.Overlay
                ' Overlay is easy, text always goes here 
                textRectangle = OverlayObjectRect(content_rect, text_size, TextAlign)
                ' Rectangle.Inflate(content_rect, -4, -4); 
                'Offset on Windows 98 style when button is pressed 
                If _state = PushButtonState.Pressed AndAlso Not Application.RenderWithVisualStyles Then
                    textRectangle.Offset(1, 1)
                End If

                ' Image is dependent on ImageAlign 
                If Image IsNot Nothing Then
                    imageRectangle = OverlayObjectRect(content_rect, image_size, ImageAlign)
                End If

                Exit Select
            Case TextImageRelation.ImageAboveText
                content_rect.Inflate(-4, -4)
                LayoutTextAboveOrBelowImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.TextAboveImage
                content_rect.Inflate(-4, -4)
                LayoutTextAboveOrBelowImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.ImageBeforeText
                content_rect.Inflate(-4, -4)
                LayoutTextBeforeOrAfterImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.TextBeforeImage
                content_rect.Inflate(-4, -4)
                LayoutTextBeforeOrAfterImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
        End Select
    End Sub

    Private Function OverlayObjectRect(ByRef container As Rectangle, ByRef sizeOfObject As Size, ByVal alignment As System.Drawing.ContentAlignment) As Rectangle
        Dim x As Integer, y As Integer

        Select Case alignment
            Case System.Drawing.ContentAlignment.TopLeft
                x = 4
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.TopCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.TopRight
                x = container.Width - sizeOfObject.Width - 4
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleLeft
                x = 4
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleRight
                x = container.Width - sizeOfObject.Width - 4
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.BottomLeft
                x = 4
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case System.Drawing.ContentAlignment.BottomCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case System.Drawing.ContentAlignment.BottomRight
                x = container.Width - sizeOfObject.Width - 4
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case Else
                x = 4
                y = 4
                Exit Select
        End Select

        Return New Rectangle(x, y, sizeOfObject.Width, sizeOfObject.Height)
    End Function

    Private Sub LayoutTextBeforeOrAfterImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
        Dim element_spacing As Integer = 0
        ' Spacing between the Text and the Image 
        Dim total_width As Integer = textSize.Width + element_spacing + imageSize.Width

        If Not textFirst Then
            element_spacing += 2
        End If

        ' If the text is too big, chop it down to the size we have available to it 
        If total_width > totalArea.Width Then
            textSize.Width = totalArea.Width - element_spacing - imageSize.Width
            total_width = totalArea.Width
        End If

        Dim excess_width As Integer = totalArea.Width - total_width
        Dim offset As Integer = 0

        Dim final_text_rect As Rectangle
        Dim final_image_rect As Rectangle

        Dim h_text As HorizontalAlignment = GetHorizontalAlignment(TextAlign)
        Dim h_image As HorizontalAlignment = GetHorizontalAlignment(ImageAlign)

        If h_image = HorizontalAlignment.Left Then
            offset = 0
        ElseIf h_image = HorizontalAlignment.Right AndAlso h_text = HorizontalAlignment.Right Then
            offset = excess_width
        ElseIf h_image = HorizontalAlignment.Center AndAlso (h_text = HorizontalAlignment.Left OrElse h_text = HorizontalAlignment.Center) Then
            offset += CInt((excess_width / 3))
        Else
            offset += CInt((2 * (excess_width / 3)))
        End If

        If textFirst Then
            final_text_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
            final_image_rect = New Rectangle(final_text_rect.Right + element_spacing, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
        Else
            final_image_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
            final_text_rect = New Rectangle(final_image_rect.Right + element_spacing, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
        End If

        textRect = final_text_rect
        imageRect = final_image_rect
    End Sub

    Private Sub LayoutTextAboveOrBelowImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
        Dim element_spacing As Integer = 0
        ' Spacing between the Text and the Image 
        Dim total_height As Integer = textSize.Height + element_spacing + imageSize.Height

        If textFirst Then
            element_spacing += 2
        End If

        If textSize.Width > totalArea.Width Then
            textSize.Width = totalArea.Width
        End If

        ' If the there isn't enough room and we're text first, cut out the image 
        If total_height > totalArea.Height AndAlso textFirst Then
            imageSize = Size.Empty
            total_height = totalArea.Height
        End If

        Dim excess_height As Integer = totalArea.Height - total_height
        Dim offset As Integer = 0

        Dim final_text_rect As Rectangle
        Dim final_image_rect As Rectangle

        Dim v_text As VerticalAlignment = GetVerticalAlignment(TextAlign)
        Dim v_image As VerticalAlignment = GetVerticalAlignment(ImageAlign)

        If v_image = VerticalAlignment.Top Then
            offset = 0
        ElseIf v_image = VerticalAlignment.Bottom AndAlso v_text = VerticalAlignment.Bottom Then
            offset = excess_height
        ElseIf v_image = VerticalAlignment.Center AndAlso (v_text = VerticalAlignment.Top OrElse v_text = VerticalAlignment.Center) Then
            offset += CInt((excess_height / 3))
        Else
            offset += CInt((2 * (excess_height / 3)))
        End If

        If textFirst Then
            final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, totalArea.Top + offset, textSize.Width, textSize.Height)
            final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, final_text_rect.Bottom + element_spacing, imageSize.Width, imageSize.Height)
        Else
            final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, totalArea.Top + offset, imageSize.Width, imageSize.Height)
            final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, final_image_rect.Bottom + element_spacing, textSize.Width, textSize.Height)

            If final_text_rect.Bottom > totalArea.Bottom Then
                final_text_rect.Y = totalArea.Top
            End If
        End If

        textRect = final_text_rect
        imageRect = final_image_rect
    End Sub

    Private Function GetHorizontalAlignment(ByVal align As System.Drawing.ContentAlignment) As HorizontalAlignment
        Select Case align
            Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.TopLeft
                Return HorizontalAlignment.Left
            Case System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.TopCenter
                Return HorizontalAlignment.Center
            Case System.Drawing.ContentAlignment.BottomRight, System.Drawing.ContentAlignment.MiddleRight, System.Drawing.ContentAlignment.TopRight
                Return HorizontalAlignment.Right
        End Select

        Return HorizontalAlignment.Left
    End Function

    Private Function GetVerticalAlignment(ByVal align As System.Drawing.ContentAlignment) As VerticalAlignment
        Select Case align
            Case System.Drawing.ContentAlignment.TopLeft, System.Drawing.ContentAlignment.TopCenter, System.Drawing.ContentAlignment.TopRight
                Return VerticalAlignment.Top
            Case System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.MiddleRight
                Return VerticalAlignment.Center
            Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.BottomRight
                Return VerticalAlignment.Bottom
        End Select

        Return VerticalAlignment.Top
    End Function

    Friend Function AlignInRectangle(ByVal outer As Rectangle, ByVal inner As Size, ByVal align As System.Drawing.ContentAlignment) As Rectangle
        Dim x As Integer = 0
        Dim y As Integer = 0

        If align = System.Drawing.ContentAlignment.BottomLeft OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.TopLeft Then
            x = outer.X
        ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.TopCenter Then
            x = Math.Max(outer.X + ((outer.Width - inner.Width) / 2), outer.Left)
        ElseIf align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.MiddleRight OrElse align = System.Drawing.ContentAlignment.TopRight Then
            x = outer.Right - inner.Width
        End If
        If align = System.Drawing.ContentAlignment.TopCenter OrElse align = System.Drawing.ContentAlignment.TopLeft OrElse align = System.Drawing.ContentAlignment.TopRight Then
            y = outer.Y
        ElseIf align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.MiddleRight Then
            y = outer.Y + (outer.Height - inner.Height) / 2
        ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.BottomLeft Then
            y = outer.Bottom - inner.Height
        End If

        Return New Rectangle(x, y, Math.Min(inner.Width, outer.Width), Math.Min(inner.Height, outer.Height))
    End Function

#End Region


    Private Sub ShowContextMenuStrip()
        State = PushButtonState.Pressed

        If m_SplitMenu IsNot Nothing Then
            m_SplitMenu.Show(Me, New Point(0, Height - 1))
        ElseIf m_SplitMenuStrip IsNot Nothing Then
            m_SplitMenuStrip.Show(Me, New Point(0, Height - 1), ToolStripDropDownDirection.BelowRight)
        End If
    End Sub

    Private Sub SplitMenuStrip_Opening(ByVal sender As Object, ByVal e As CancelEventArgs)
        isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenuStrip_Closing(ByVal sender As Object, ByVal e As ToolStripDropDownClosingEventArgs)
        isSplitMenuVisible = False

        'if the user clicks outside the button, set the button state 
        If Not Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            SetButtonDrawState()
        End If
    End Sub

    Private Sub SplitMenu_Popup(ByVal sender As Object, ByVal e As EventArgs)
        isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenu_Collapse()
        isSplitMenuVisible = False

        'if the user clicks outside the button, set the button state 
        If Not Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            SetButtonDrawState()
        End If
    End Sub

    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overloads Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = 530 Then
            'this message is only sent when a ContextMenu is closed (not a ContextMenuStrip) 
            SplitMenu_Collapse()
        End If

        MyBase.WndProc(m)
    End Sub

    Private Sub SetButtonDrawState()
        If Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            State = PushButtonState.Hot
        ElseIf Focused Then
            State = PushButtonState.[Default]
        ElseIf Not Enabled Then
            State = PushButtonState.Disabled
        Else
            State = PushButtonState.Normal
        End If
    End Sub
End Class

''' <summary>
''' Drop-down combo box automatically filled with non-system colors
''' </summary>
''' <remarks>Adapted from similar control for datagridview column</remarks>
Public Class ColorPickerCombo
    Inherits ComboBox

    Public Sub New()
        MyBase.New()
        If IsDesignMode(Me) Then Exit Sub
        DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
        'add standard, non-system colors to sorted list
        Sorted = False
        DropDownStyle = ComboBoxStyle.DropDownList
        Items.Clear()
        'Dim SysColors() As String = {"Active", "App", "Button", "Control", "Desktop", "Gradient", "GrayText", "Highlight", "Hot", "Inactive", "Info", "Menu", "Scroll", "Window"}
        'For Each c As String In [Enum].GetNames(GetType(KnownColor))
        '    Dim IsSys As Boolean = False
        '    For Each s As String In SysColors
        '        If c.StartsWith(s) Then IsSys = True : Exit For
        '    Next
        '    If Not IsSys Then Items.Add(c)
        'Next

        'instead of returning colors in alphabetical order, sort by similar colors
        For Each c As Drawing.Color In SortedColors()
            Items.Add(c.Name)
        Next
    End Sub

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' See also: http://groups.google.com/group/microsoft.public.dotnet.framework.windowsforms.controls/browse_thread/thread/6c75aaa3bc23a954/4d6f4c0ff1583e1a?q=group:*dotnet*+%22sort+colors%22#4d6f4c0ff1583e1a
    ''' </summary>
    ''' <remarks></remarks>
    Private Class HSB_ColorComparer
        Implements IComparer(Of Color)

        Public Function Compare(ByVal c1 As Color, ByVal c2 As Color) As Integer Implements IComparer(Of System.Drawing.Color).Compare
            If c1 = Color.Transparent And c2 = Color.Transparent Then
                Return 0
            ElseIf c1 = Color.Transparent Then
                Return -1
            ElseIf c2 = Color.Transparent Then
                Return 1
            End If
            If c1.GetHue() > c2.GetHue() Then
                Return 1
            ElseIf c1.GetHue() < c2.GetHue() Then
                Return -1
            Else
                If c1.GetSaturation() > c2.GetSaturation() Then
                    Return 1
                ElseIf c1.GetSaturation() < c2.GetSaturation() Then
                    Return -1
                Else
                    If c1.ToArgb() > c2.ToArgb() Then
                        Return 1
                    ElseIf c1.ToArgb() < c2.ToArgb() Then
                        Return -1
                    Else
                        Return 0
                    End If
                End If
            End If
        End Function

    End Class

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' </summary>
    ''' <returns>Sorted arraylist of colors in order of that used in VS</returns>
    ''' <remarks></remarks>
    Private Function SortedColors() As Generic.List(Of Drawing.Color)
        Dim lstColors As New Generic.List(Of System.Drawing.Color)
        Dim colorType As Type = GetType(System.Drawing.Color)
        For Each pi As System.Reflection.PropertyInfo In colorType.GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.Static)
            If pi.PropertyType.Equals(colorType) Then
                lstColors.Add(CType(pi.GetValue(colorType, Nothing), System.Drawing.Color))
            End If
        Next
        Dim HSB_Comparer As New HSB_ColorComparer
        lstColors.Sort(HSB_Comparer)
        Return lstColors
    End Function

    ''' <summary>
    ''' This prevents the list from being filled twice (once by designer, then again later)
    ''' </summary>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(False), EditorBrowsable(EditorBrowsableState.Never)> _
    Overloads Property Items()
        Get
            Return MyBase.Items
        End Get
        Set(ByVal value)

        End Set
    End Property

    Protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
        MyBase.OnDrawItem(e)
        If e.Index = -1 Then Exit Sub
        Dim rect As New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, e.Bounds.Width - 2, e.Bounds.Height - 2)
        Dim BlockColor As Color = Color.FromName(Items(e.Index))
        DrawColorItem(e.Graphics, rect, BlockColor, Me.Font)
    End Sub

    Friend Shared Sub DrawColorItem(ByVal g As Graphics, ByVal Rect As Rectangle, ByVal ItemColor As Color, ByVal ItemFont As Font)
        'draw rectangle filled with color; on top draw color name text, setting text color as black or white to give good contrast
        Dim sf As New StringFormat
        sf = New StringFormat()
        sf.LineAlignment = StringAlignment.Center
        sf.Alignment = StringAlignment.Center
        g.FillRectangle(New SolidBrush(ItemColor), Rect)
        g.DrawRectangle(Pens.Black, Rect)
        Dim b As Brush = Brushes.Black
        Dim DarkColors() As String = {"Black", "Blue", "Brown", "Crimson", "Dark", "Deep", "Dim", "Dodger", "Forest", "Fuchsia", "Gray", "Green", "Indigo", "Magenta", "Maroon", "Medium", "Midnight", "Navy", "Olive", "Orange", "Purple", "Red", "Rosy", "Royal", "SeaGreen", "Sienna", "Slate", "Steel", "Teal"}
        For Each s As String In DarkColors
            If ItemColor.Name.StartsWith(s) Then b = Brushes.White : Exit For
        Next
        g.DrawString(ItemColor.Name, ItemFont, b, Rect, sf)
        sf.Dispose()
    End Sub
End Class


''' <summary>
''' Specialized version of Listview, intended to show multi-column detail list
''' </summary>
''' <remarks></remarks>
Public Class MyListView
    Inherits ListView

    Public Sub New()
        MyBase.New()
        Activation = ItemActivation.Standard
        CheckBoxes = True
        FullRowSelect = True
        GridLines = True
        HeaderStyle = ColumnHeaderStyle.Nonclickable
        MultiSelect = False
        View = View.Details
        HideSelectedRow = True
    End Sub

    ''' <summary>
    ''' If true, row selection will be hidden for listviews having checkboxes, as this can be confusing (will still have dotted highlight)
    ''' </summary>
    Public Property HideSelectedRow As Boolean

    Public Property CheckOnClick As Boolean

    ''' <summary>
    ''' When check boxes used, don't allow row to appear highlighted
    ''' </summary>
    Private Sub MyListView_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.SelectedIndexChanged
        If CheckBoxes Then
            If CheckOnClick AndAlso MyBase.SelectedItems.Count = 1 Then MyBase.SelectedItems(0).Checked = Not MyBase.SelectedItems(0).Checked
            If _HideSelectedRow Then MyBase.SelectedItems.Clear()
        End If
    End Sub

    ''' <summary>
    ''' Initialize control, adding the specified columns left aligned and setting defaults:
    ''' 
    ''' </summary>
    ''' <param name="ColumnNames">Name of column heading</param>
    ''' <remarks></remarks>
    Public Sub AddColumns(ByVal ParamArray ColumnNames() As String)
        Clear()
        For i As Integer = 0 To ColumnNames.Length - 1
            Columns.Add(ColumnNames(i), IIf(i = ColumnNames.Length - 1, -2, -1), HorizontalAlignment.Left)
        Next
    End Sub

    ''' <summary>
    ''' Add row of text items and invisible tag associated with row
    ''' </summary>
    Public Sub AddRow(ByVal Tag As Object, ByVal ParamArray _Items() As String)
        With Items.Add(_Items(0))
            For i As Integer = 1 To _Items.Length - 1
                .SubItems.Add(_Items(i))
            Next
            .Tag = Tag
        End With
    End Sub

    Public Overloads Sub AutoResizeColumns(ByVal ExpandLast As Boolean)
        MyBase.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent)
        If ExpandLast And Columns.Count > 0 Then Columns(Columns.Count - 1).Width = -2
    End Sub

    Public Sub CheckAll(ByVal Checked As Boolean)
        If Not CheckBoxes Then Exit Sub
        For i As Integer = 0 To Items.Count - 1
            Items(i).Checked = Checked
        Next
    End Sub
End Class

''' <summary>
''' Adapted from http://www.codeproject.com/Articles/31105/A-ComboBox-with-a-CheckedListBox-as-a-Dropdown
''' </summary>
''' <remarks>Can set AllUncheckedText property to indicate text to be displayed when no items checked</remarks>
Public Class CheckedComboBox
    Inherits ComboBox

    Private WithEvents MyContextMenu As ContextMenuStrip = Nothing

    ''' <summary>
    ''' Following avoids problem with initial click getting swallowed
    ''' </summary>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
        MyBase.OnMouseDown(e)
        DroppedDown = False
    End Sub

    ''' <summary>
    ''' Internal class to represent the dropdown list of the CheckedComboBox
    ''' </summary>
    Friend Shadows Class Dropdown
        Inherits Form
        ' ---------------------------------- internal class CCBoxEventArgs --------------------------------------------
        ''' <summary>
        ''' Custom EventArgs encapsulating value as to whether the combo box value(s) should be assigned to or not.
        ''' </summary>
        Friend Class CCBoxEventArgs
            Inherits EventArgs
            Private m_assignValues As Boolean
            Public Property AssignValues() As Boolean
                Get
                    Return m_assignValues
                End Get
                Set(value As Boolean)
                    m_assignValues = value
                End Set
            End Property
            Private e As EventArgs
            Public Property EventArgs() As EventArgs
                Get
                    Return e
                End Get
                Set(value As EventArgs)
                    e = value
                End Set
            End Property
            Public Sub New(e As EventArgs, assignValues As Boolean)
                MyBase.New()
                Me.e = e
                Me.m_assignValues = assignValues
            End Sub
        End Class

        ' ---------------------------------- internal class CustomCheckedListBox --------------------------------------------

        ''' <summary>
        ''' A custom CheckedListBox being shown within the dropdown form representing the dropdown list of the CheckedComboBox.
        ''' </summary>
        Friend Class CustomCheckedListBox
            Inherits CheckedListBox
            Private curSelIndex As Integer = -1

            Public Sub New()
                MyBase.New()
                Me.SelectionMode = SelectionMode.One
                Me.HorizontalScrollbar = True
            End Sub

            ''' <summary>
            ''' Intercepts the keyboard input, [Enter] confirms a selection and [Esc] cancels it.
            ''' </summary>
            ''' <param name="e">The Key event arguments</param>
            Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
                If e.KeyCode = Keys.Enter Then
                    ' Enact selection.
                    DirectCast(Parent, CheckedComboBox.Dropdown).OnDeactivate(New CCBoxEventArgs(Nothing, True))

                    e.Handled = True
                ElseIf e.KeyCode = Keys.Escape Then
                    ' Cancel selection.
                    DirectCast(Parent, CheckedComboBox.Dropdown).OnDeactivate(New CCBoxEventArgs(Nothing, False))

                    e.Handled = True
                ElseIf e.KeyCode = Keys.Delete Then
                    ' Delete unchecks all, [Shift + Delete] checks all.
                    For i As Integer = 0 To Items.Count - 1
                        SetItemChecked(i, e.Shift)
                    Next
                    e.Handled = True
                End If
                ' If no Enter or Esc keys presses, let the base class handle it.
                MyBase.OnKeyDown(e)
            End Sub

            Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
                MyBase.OnMouseMove(e)
                Dim index As Integer = IndexFromPoint(e.Location)
                Debug.WriteLine("Mouse over item: " + (If(index >= 0, GetItemText(Items(index)), "None")))
                If (index >= 0) AndAlso (index <> curSelIndex) Then
                    curSelIndex = index
                    SetSelected(index, True)
                End If
            End Sub

        End Class
        ' end internal class CustomCheckedListBox
        ' --------------------------------------------------------------------------------------------------------

        ' ********************************************* Data *********************************************

        Private WithEvents ccbParent As CheckedComboBox

        ' Keeps track of whether checked item(s) changed, hence the value of the CheckedComboBox as a whole changed.
        ' This is simply done via maintaining the old string-representation of the value(s) and the new one and comparing them!
        Private oldStrValue As String = ""
        Public ReadOnly Property ValueChanged() As Boolean
            Get
                Dim newStrValue As String = ccbParent.Text
                If (oldStrValue.Length > 0) AndAlso (newStrValue.Length > 0) Then
                    Return (oldStrValue.CompareTo(newStrValue) <> 0)
                Else
                    Return (oldStrValue.Length <> newStrValue.Length)
                End If
            End Get
        End Property

        ' Array holding the checked states of the items. This will be used to reverse any changes if user cancels selection.
        Private checkedStateArr As Boolean()

        ' Whether the dropdown is closed.
        Private dropdownClosed As Boolean = True

        Private tt As ToolTip

        Private cclb As CustomCheckedListBox
        Public Property List() As CustomCheckedListBox
            Get
                Return cclb
            End Get
            Set(value As CustomCheckedListBox)
                cclb = value
            End Set
        End Property

        ' ********************************************* Construction *********************************************

        Public Sub New(ccbParent As CheckedComboBox)
            Me.ccbParent = ccbParent
            InitializeComponent()
            Me.ShowInTaskbar = False
            ' Add a handler to notify our parent of ItemCheck events.
            AddHandler Me.cclb.ItemCheck, New System.Windows.Forms.ItemCheckEventHandler(AddressOf Me.cclb_ItemCheck)
        End Sub

        ' ********************************************* Methods *********************************************

        ' Create a CustomCheckedListBox which fills up the entire form area.
        Private Sub InitializeComponent()

            Me.cclb = New CustomCheckedListBox()
            Me.SuspendLayout()
            ' 
            ' cclb
            ' 
            Me.cclb.BorderStyle = System.Windows.Forms.BorderStyle.None
            Me.cclb.Dock = System.Windows.Forms.DockStyle.Fill
            Me.cclb.FormattingEnabled = True
            Me.cclb.Location = New System.Drawing.Point(0, 0)
            Me.cclb.Name = "cclb"
            Me.cclb.Size = New System.Drawing.Size(47, 15)
            Me.cclb.TabIndex = 0

            ' 
            ' Dropdown
            ' 
            Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0F, 13.0F)
            Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
            Me.BackColor = System.Drawing.SystemColors.Menu
            Me.ClientSize = New System.Drawing.Size(47, 16)
            Me.ControlBox = False
            Me.Controls.Add(Me.cclb)
            Me.ForeColor = System.Drawing.SystemColors.ControlText
            Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow
            Me.MinimizeBox = False
            Me.Name = "ccbParent"
            Me.StartPosition = System.Windows.Forms.FormStartPosition.Manual
            Me.ResumeLayout(False)
            Me.Text = GetCheckedItemsStringValue()

            Me.tt = New ToolTip
            tt.SetToolTip(Me.cclb, "Press Del key to clear all checks; Shift-Del to set all checks; ESC to cancel; Return to accept; Rt-Click for menu")

        End Sub

        Public Function GetCheckedItemsStringValue() As String
            Dim sb As New StringBuilder("")
            If cclb Is Nothing OrElse cclb.CheckedItems.Count = 0 Then
                sb.Append(ccbParent.AllUncheckedText)
            Else
                For i As Integer = 0 To cclb.CheckedItems.Count - 1
                    sb.Append(cclb.GetItemText(cclb.CheckedItems(i))).Append(ccbParent.ValueSeparator)
                Next
                If sb.Length > 0 Then
                    sb.Remove(sb.Length - ccbParent.ValueSeparator.Length, ccbParent.ValueSeparator.Length)
                End If
            End If
            Return sb.ToString()
        End Function

        ''' <summary>
        ''' Given list of values such as that displayed as text in control, check the matching items
        ''' </summary>
        ''' <param name="ItemList">The list of selected items (usually comma separated)</param>
        Public Sub SetCheckedItems(ItemList As String)
            For i As Integer = 0 To cclb.Items.Count - 1
                cclb.SetItemChecked(i, False)
            Next
            For Each s As String In StringSplit(ItemList, ccbParent.ValueSeparator, True)
                For i As Integer = 0 To cclb.Items.Count - 1
                    If ccbParent.Items(i).ToString = s Then cclb.SetItemChecked(i, True)
                Next
            Next
        End Sub

        ''' <summary>
        ''' Closes the dropdown portion and enacts any changes according to the specified boolean parameter.
        ''' NOTE: even though the caller might ask for changes to be enacted, this doesn't necessarily mean
        '''       that any changes have occurred as such. Caller should check the ValueChanged property of the
        '''       CheckedComboBox (after the dropdown has closed) to determine any actual value changes.
        ''' </summary>
        ''' <param name="enactChanges"></param>
        Public Sub CloseDropdown(enactChanges As Boolean)
            If dropdownClosed Then
                Return
            End If
            Debug.WriteLine("CloseDropdown")
            ' Perform the actual selection and display of checked items.
            If enactChanges Then
                ccbParent.SelectedIndex = -1
                ' Set the text portion equal to the string comprising all checked items (if any, otherwise empty!).

                ccbParent.Text = GetCheckedItemsStringValue()
            Else
                ' Caller cancelled selection - need to restore the checked items to their original state.
                For i As Integer = 0 To cclb.Items.Count - 1
                    cclb.SetItemChecked(i, checkedStateArr(i))
                Next
            End If
            ' From now on the dropdown is considered closed. We set the flag here to prevent OnDeactivate() calling
            ' this method once again after hiding this window.
            dropdownClosed = True
            ' Set the focus to our parent CheckedComboBox and hide the dropdown check list. (Don't do this; it highlights all the text)
            ccbParent.Focus()
            Me.Hide()
            ' Notify CheckedComboBox that its dropdown is closed. (NOTE: it does not matter which parameters we pass to
            ' OnDropDownClosed() as long as the argument is CCBoxEventArgs so that the method knows the notification has
            ' come from our code and not from the framework).
            ccbParent.OnDropDownClosed(New CCBoxEventArgs(Nothing, False))
        End Sub

        Protected Overrides Sub OnActivated(e As EventArgs)
            Debug.WriteLine("OnActivated")
            MyBase.OnActivated(e)
            dropdownClosed = False
            ' Assign the old string value to compare with the new value for any changes.
            oldStrValue = ccbParent.Text
            ' Make a copy of the checked state of each item, in cace caller cancels selection.
            checkedStateArr = New Boolean(cclb.Items.Count - 1) {}
            For i As Integer = 0 To cclb.Items.Count - 1
                checkedStateArr(i) = cclb.GetItemChecked(i)
            Next
        End Sub

        Protected Overrides Sub OnDeactivate(e As EventArgs)
            Debug.WriteLine("OnDeactivate")
            MyBase.OnDeactivate(e)
            Dim ce As CCBoxEventArgs = TryCast(e, CCBoxEventArgs)
            If ce IsNot Nothing Then

                CloseDropdown(ce.AssignValues)
            Else
                ' If not custom event arguments passed, means that this method was called from the
                ' framework. We assume that the checked values should be registered regardless.
                CloseDropdown(True)
            End If
        End Sub

        Private Sub cclb_ItemCheck(sender As Object, e As ItemCheckEventArgs)
            If ccbParent IsNot Nothing Then
                ccbParent_ItemCheck(sender, e)
            End If
        End Sub

        Private Sub ccbParent_ItemCheck(sender As Object, e As ItemCheckEventArgs) Handles ccbParent.ItemCheck

        End Sub
    End Class

    ' end internal class Dropdown
    ' ******************************** Data ********************************
    ''' <summary>
    ''' Required designer variable.
    ''' </summary>
    Private components As System.ComponentModel.IContainer = Nothing
    ' A form-derived object representing the drop-down list of the checked combo box.
    Private dropdownctl As Dropdown

    ' The valueSeparator character(s) between the ticked elements as they appear in the 
    ' text portion of the CheckedComboBox.
    Private m_valueSeparator As String
    Public Property ValueSeparator() As String
        Get
            Return m_valueSeparator
        End Get
        Set(value As String)
            m_valueSeparator = value
        End Set
    End Property

    Private _AllUncheckedText As String = ""
    ''' <summary>
    ''' This is the text that will appear if not items are checked
    ''' </summary>
    Public Property AllUncheckedText As String
        Set(value As String)
            _AllUncheckedText = value
            Me.Text = dropdownctl.GetCheckedItemsStringValue()
        End Set
        Get
            Return _AllUncheckedText
        End Get
    End Property

    Public Property CheckOnClick() As Boolean
        Get
            Return dropdownctl.List.CheckOnClick
        End Get
        Set(value As Boolean)
            dropdownctl.List.CheckOnClick = value
        End Set
    End Property

    Public Shadows Property DisplayMember() As String
        Get
            Return dropdownctl.List.DisplayMember
        End Get
        Set(value As String)
            dropdownctl.List.DisplayMember = value
        End Set
    End Property

    Public Shadows ReadOnly Property Items() As CheckedListBox.ObjectCollection
        Get
            Return dropdownctl.List.Items
        End Get
    End Property

    Public ReadOnly Property CheckedItems() As CheckedListBox.CheckedItemCollection
        Get
            Return dropdownctl.List.CheckedItems
        End Get
    End Property

    Public ReadOnly Property CheckedIndices() As CheckedListBox.CheckedIndexCollection
        Get
            Return dropdownctl.List.CheckedIndices
        End Get
    End Property

    Public ReadOnly Property ValueChanged() As Boolean
        Get
            Return dropdownctl.ValueChanged
        End Get
    End Property

    ' Event handler for when an item check state changes.
    Public Event ItemCheck As ItemCheckEventHandler

    ' ******************************** Construction ********************************

    Public Sub New()
        MyBase.New()
        ' We want to do the drawing of the dropdown.
        Me.DrawMode = DrawMode.OwnerDrawVariable
        ' Default value separator.
        Me.m_valueSeparator = ", "
        ' This prevents the actual ComboBox dropdown to show, although it's not strickly-speaking necessary.
        ' But including this remove a slight flickering just before our dropdown appears (which is caused by
        ' the empty-dropdown list of the ComboBox which is displayed for fractions of a second).
        Me.DropDownHeight = 1
        ' This is the default setting - text portion is editable and user must click the arrow button
        ' to see the list portion. Although we don't want to allow the user to edit the text portion
        ' the DropDownList style is not being used because for some reason it wouldn't allow the text
        ' portion to be programmatically set. Hence we set it as editable but disable keyboard input (see below).
        Me.DropDownStyle = ComboBoxStyle.DropDown
        Me.dropdownctl = New Dropdown(Me)
        ' CheckOnClick style for the dropdown (NOTE: must be set after dropdown is created).
        Me.CheckOnClick = True

        'add context menu to check all/none
        MyContextMenu = New ContextMenuStrip
        With MyContextMenu.Items
            .Add("Check All", My.Resources.CheckBoxHS)
            .Add("Check None", My.Resources.CheckBoxHS2)
        End With

        Me.ContextMenuStrip = MyContextMenu
        dropdownctl.ContextMenuStrip = MyContextMenu
    End Sub

    ' ******************************** Operations ********************************

    ''' <summary>
    ''' Clean up any resources being used.
    ''' </summary>
    ''' <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    Protected Overrides Sub Dispose(disposing As Boolean)
        If disposing AndAlso (components IsNot Nothing) Then
            components.Dispose()
        End If
        MyBase.Dispose(disposing)
    End Sub

    Protected Overrides Sub OnDropDown(e As EventArgs)
        MyBase.OnDropDown(e)
        DoDropDown()
    End Sub

    Private Sub DoDropDown()
        If Not dropdownctl.Visible Then
            Dim rect As Rectangle = RectangleToScreen(Me.ClientRectangle)
            dropdownctl.Location = New Point(rect.X, rect.Y + Me.Size.Height)
            Dim count As Integer = dropdownctl.List.Items.Count
            If count > Me.MaxDropDownItems Then
                count = Me.MaxDropDownItems
            ElseIf count = 0 Then
                count = 1
            End If
            dropdownctl.Size = New Size(Me.Size.Width, (dropdownctl.List.ItemHeight) * count + 2)
            dropdownctl.Show(Me)
        End If
    End Sub

    Protected Overrides Sub OnDropDownClosed(e As EventArgs)
        ' Call the handlers for this event only if the call comes from our code - NOT the framework's!
        ' NOTE: that is because the events were being fired in a wrong order, due to the actual dropdown list
        '       of the ComboBox which lies underneath our dropdown and gets involved every time.
        If TypeOf e Is Dropdown.CCBoxEventArgs Then
            MyBase.OnDropDownClosed(e)
        End If
    End Sub

    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        If e.KeyCode = Keys.Down Then
            ' Signal that the dropdown is "down". This is required so that the behaviour of the dropdown is the same
            ' when it is a result of user pressing the Down_Arrow (which we handle and the framework wouldn't know that
            ' the list portion is down unless we tell it so).
            ' NOTE: all that so the DropDownClosed event fires correctly!                
            OnDropDown(Nothing)
        End If
        ' Make sure that certain keys or combinations are not blocked.
        e.Handled = Not e.Alt AndAlso Not (e.KeyCode = Keys.Tab) AndAlso Not ((e.KeyCode = Keys.Left) OrElse (e.KeyCode = Keys.Right) OrElse (e.KeyCode = Keys.Home) OrElse (e.KeyCode = Keys.[End]))

        MyBase.OnKeyDown(e)
    End Sub

    Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
        e.Handled = True
        MyBase.OnKeyPress(e)
    End Sub

    Protected Overrides Sub OnClick(e As EventArgs)
        'just clicking on the textbox causes the list to appear
        MyBase.OnClick(e)
        DoDropDown()
    End Sub

    Public Function GetItemChecked(index As Integer) As Boolean
        If index < 0 OrElse index > Items.Count Then
            Throw New ArgumentOutOfRangeException("index", "value out of range")
        Else
            Return dropdownctl.List.GetItemChecked(index)
        End If
    End Function

    Public Sub SetItemChecked(index As Integer, isChecked As Boolean)
        If index < 0 OrElse index > Items.Count Then
            Throw New ArgumentOutOfRangeException("index", "value out of range")
        Else
            dropdownctl.List.SetItemChecked(index, isChecked)
            ' Need to update the Text.
            Me.Text = dropdownctl.GetCheckedItemsStringValue()
        End If
    End Sub

    ''' <summary>
    ''' Sets checkboxes based on comma-separated list of item names
    ''' </summary>
    Public Property CheckedItemsString As String
        Get
            Return Me.Text
        End Get
        Set(value As String)
            dropdownctl.SetCheckedItems(value)
            Me.Text = dropdownctl.GetCheckedItemsStringValue()
        End Set
    End Property

    Public Function GetItemCheckState(index As Integer) As CheckState
        If index < 0 OrElse index > Items.Count Then
            Throw New ArgumentOutOfRangeException("index", "value out of range")
        Else
            Return dropdownctl.List.GetItemCheckState(index)
        End If
    End Function

    Public Sub SetItemCheckState(index As Integer, state As CheckState)
        If index < 0 OrElse index > Items.Count Then
            Throw New ArgumentOutOfRangeException("index", "value out of range")
        Else
            dropdownctl.List.SetItemCheckState(index, state)
            ' Need to update the Text.
            Me.Text = dropdownctl.GetCheckedItemsStringValue()
        End If
    End Sub

    Private Sub MyContextMenu_ItemClicked(sender As Object, e As ToolStripItemClickedEventArgs) Handles MyContextMenu.ItemClicked
        For i As Integer = 0 To Items.Count - 1
            SetItemChecked(i, MyContextMenu.Items.IndexOf(e.ClickedItem) = 0)
        Next
    End Sub
End Class
